package org.gwt.project.tools;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.jdt.core.IClasspathContainer;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.ui.jarpackager.IJarBuilder;
import org.eclipse.jdt.ui.jarpackager.JarPackageData;
import org.gwt.project.tools.ToolsConfig.Factory;
import org.gwt.project.tools.cpe.Container;
import org.gwt.project.tools.quickfix.Generator;

/**
 * This builder must always BEFORE any java builders.
 * 
 * @author kengu
 *
 */
public final class ToolsBuilder extends IncrementalProjectBuilder {

	public static final String ID = ToolsPlugin.ID + ".builder";
	
	public static final QualifiedName DERIVED = new QualifiedName(ToolsBuilder.ID, "derived");
	
	private static final String TRUE = "true";
	
	protected ToolsConfig config = new Factory().create();

	class Visitor implements IResourceDeltaVisitor {

		private IProgressMonitor monitor;
		
		public Visitor(IProgressMonitor monitor) {
			this.monitor = monitor;
		}
		
		@Override
		public boolean visit(IResourceDelta delta) throws CoreException {
			// Interested?
			if (isLib(delta)) {				
				// Delete old markers created by this builder
				deleteMarkers(getProject());
				// Synchronize this project with changes in given project
				build(delta.getResource(),monitor);
				// Finished
				return false;
				
			} else if (isSource(delta)) {
				
				
				
				// Finished
				return false;
				
			}
			// Continue
			return true;
		}
	}

	@Override @SuppressWarnings("rawtypes") 
	protected IProject[] build(int kind, Map args, 
			IProgressMonitor monitor) throws CoreException {
		IProject[] referenced = getProject().getReferencedProjects();
		if (kind == FULL_BUILD) {
			fullBuild(monitor);
		} else {
			// Is this project changed?
			IResourceDelta delta = getDelta(getProject());
			if (delta == null) {
				fullBuild(monitor);
			} else if (delta.getKind() != 0){
				incrementalBuild(delta, monitor);
			} else {
				for(IProject it : referenced) {
					delta = getDelta(it);
					if(isSource(delta)) {
						// Listen for POST_BUILD, build jar from 
						// referenced project source and add to source 
						// lib folder in this project
						new PostBuildHook(getProject()).handle();
					}
				}
			}
		}
		return referenced;
	}
	
	@Override
	protected void clean(IProgressMonitor monitor) throws CoreException {
		// Delete all markers created by this builder
		deleteMarkers(getProject());
		// TODO: Remove all user libs from web application 
	}
	
	private boolean isLib(IResourceDelta delta) throws CoreException {
		// Only interested in following changes
		switch(delta.getKind()) {
		case IResourceDelta.ADDED:
		case IResourceDelta.REMOVED:
		case IResourceDelta.CHANGED:
		case IResourceDelta.MOVED_TO:
		case IResourceDelta.MOVED_FROM:
			// Only interested in user lib dependency changes
			return !isReferencedProjectLib(delta) && isLibFile(delta.getResource());
		}
		// No match
		return false;
	}		
		
	private boolean isLibFolder(IResource resource) {
		// Only interested "lib" folders 
		IResource parent = resource.getParent();
		return (parent instanceof IFolder) && 
				parent.getName().equals(config.getSrcLib());
	}

	private boolean isLibFile(IResource resource) throws CoreException {
		// Only interested in jars in "lib" folders 
		return !isReferencedProjectLib(resource) && isLibFolder(resource) && ToolsUtil.isJar(resource);
	}
	
	private boolean isReferencedProjectLib(IResourceDelta delta) throws CoreException {
		IResource resource = delta.getResource();
		if(resource instanceof IFile) {
			return isReferencedProjectLib(resource);
		}
		for(IResourceDelta it : delta.getAffectedChildren()) {
			if(isReferencedProjectLib(it)) return true;
		}
		return false;
	}
	
	private boolean isReferencedProjectLib(IResource resource) throws CoreException {
		return resource!=null && resource.isAccessible() && TRUE.equals(resource.getPersistentProperty(DERIVED));
	}
	
	private boolean isSource(IResourceDelta delta) throws CoreException {
		// Only interested in following changes
		switch(delta.getKind()) {
		case IResourceDelta.ADDED:
		case IResourceDelta.REMOVED:
		case IResourceDelta.CHANGED:
		case IResourceDelta.MOVED_TO:
		case IResourceDelta.MOVED_FROM:
			
			// Check if delta is located in the output folder
			IResource resource = delta.getResource();

			if(resource instanceof IFolder) {
				return isOutputFolder(resource);
			}
			for(IResourceDelta it : delta.getAffectedChildren()) {
				if(isSource(it)) return true;
			}			
	    }
		// Failure
		return false;
	}		
	
	private boolean isOutputFolder(IResource resource) throws JavaModelException {
        IJavaProject project = JavaCore.create(resource.getProject());
        IPath output = project.getOutputLocation();
        return resource.getFullPath().equals(output);
	}
	
	private void deleteMarkers(IResource resource) throws CoreException {
		resource.deleteMarkers(Generator.PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
	}

	private void fullBuild(final IProgressMonitor monitor) 
			throws CoreException {
		// Delegate work to visitor (assume all is added)
		build(getProject(), monitor);
	}

	private void incrementalBuild(IResourceDelta delta,
			IProgressMonitor monitor) throws CoreException {
		// Delegate work to visitor
		delta.accept(new Visitor(monitor));
	}
	
	private List<IResource> build(
			IResource resource, 
			IProgressMonitor monitor)
			throws CoreException {
		
		// Get resource
		IProject project = getProject();
		
		// Get source libraries in given and all referenced projects
		List<IResource> files = ToolsUtil.getSrcLibs(project, config);

		// Copy or delete?
		List<IResource> modified;
		if(!resource.isAccessible()) {
			// Remove deleted user jars from given project
			modified = delete(resource, config, monitor);
		} else {
			// Copy missing user jars to given project
			modified =  copy(files, config, monitor);
		}
		
		// Create java project
		IJavaProject jp = JavaCore.create(project);
		
		// Force new reference between project and container instance 
        JavaCore.setClasspathContainer(
        		Container.PATH, 
        		new IJavaProject[] { jp },
        		new IClasspathContainer[] { null }, 
        		monitor);
		
		// Finished
		return modified;
		
	}
	
	public List<IResource> copy(
			List<IResource> members, 
			ToolsConfig config,
			IProgressMonitor monitor) throws CoreException {
		
		// Get folders
		IFolder dst = getProject().getFolder(config.getDstLib());
		
		// Detect library conflict
		List<IResource> conflicts = new ArrayList<IResource>();
		Set<String> names = new HashSet<String>();
		for(IResource it : members) {
			// Get library name
			String name = it.getName();
			// Class-path conflict found?
			if(names.contains(name)) {
				
				// Create duplicate warnings
				duplicate(getProject(), name, config);
				
				// Add to conflicts
				conflicts.add(it);
			}			
			// Cache library name
			names.add(name);
		}
		
		// Copy files
		members = new ArrayList<IResource>(members);
		
		// Remove conflicts
		members.removeAll(conflicts);
		
		// Finished
		return ToolsUtil.copy(members, dst, monitor);
		
	}
	
	private IResource duplicate(IProject project, String library, ToolsConfig config) throws CoreException {
		
		// Get resource
		IResource resource = project.getFolder(config.getSrcLib()).getFile(library);
		
		// Create marker on resource
		Generator.createProblemMarker(
				resource, 
				Generator.CONTAINER,
				"Duplicate library name. Already added by referenced project",
				IMarker.PRIORITY_HIGH, 
				IMarker.SEVERITY_WARNING, 
				Container.DESCRIPTION);
		
		// Finished
		return resource;
	}
	
	private List<IResource> delete(IResource resource, ToolsConfig config,
			IProgressMonitor monitor) throws CoreException {
		
		// Initialize
		List<IResource> deleted = new ArrayList<IResource>(1);
		
		// Get deleted libs
		if(resource instanceof IProject) {
			deleted.addAll(ToolsUtil.getSrcLibs(resource.getProject(), config));
		} else if(resource instanceof IFolder) {
			deleted.addAll(ToolsUtil.getLibs((IFolder)resource));
		} else {
			deleted.add(resource);
		}

		// Get folder
		IFolder dst = getProject().getFolder(config.getDstLib());
		
		// Delete resources
		return ToolsUtil.delete(deleted, dst, monitor);
		
	}
	
	private static class PostBuildHook implements IResourceChangeListener {
		
		private final IProject project;
		private final IWorkspace workspace = ResourcesPlugin.getWorkspace();
		
		public PostBuildHook(IProject project) {
			this.project = project;
		}
		
		public void handle() {
			// Add build hook
			workspace.addResourceChangeListener(this, IResourceChangeEvent.POST_BUILD);			
		}

		private void release() {
			// Add build hook
			workspace.removeResourceChangeListener(this);			
		}
		
		@Override
		public void resourceChanged(IResourceChangeEvent event) {
			// Interested?
			if( event.getType() == IResourceChangeEvent.POST_BUILD ) {
				// Check all projects
				for(IResourceDelta it : event.getDelta().getAffectedChildren()) {
					try {
						// Get previously built project
						IProject built = it.getResource().getProject();
						// Has required nature?
						if(!project.equals(built) && project.hasNature(ToolsNature.ID)) {
							// Initialize
							IProgressMonitor monitor = new NullProgressMonitor();
							// Export project as jar to <war>/WEB-INF/lib
							ToolsConfig config = new ToolsConfig();
							// Get project name
							String name = built.getName();
							// Get jar folder (make it if not exist)
							IFolder srcLib = ToolsUtil.ensure(project.getFolder(config.getSrcLib()),monitor);							
							// Get jar destination					
							IPath dst = srcLib.getProjectRelativePath().append(name+".jar");
							// Delete current jar
							if(project.exists(dst)) project.getFile(dst).delete(true, monitor);
							// Get output folder in built folder
							IJavaProject jp = JavaCore.create(built);
							IPath output = jp.getOutputLocation();
							IFolder folder = built.getWorkspace().getRoot().getFolder(output);
							IFile[] classes = ToolsUtil.toClasses(folder);
							if(classes.length>0) createJar(project, jp, dst, classes, monitor);
						}
					} catch (CoreException e) {
						ToolsPlugin.log("Failed to export jars to '" + project.getName() + "'", e);
					}
				}
			}
			// Cleanup
			release();
		}

		private void createJar(IProject project, IJavaProject referenced, IPath dst, IFile[] classes, IProgressMonitor monitor) throws CoreException {
	        
			// Prepare jar data
			JarPackageData desc= new JarPackageData();
	        desc.setJarLocation(project.getLocation().append(dst));
	        desc.setSaveManifest(true);
	        desc.setGenerateManifest(true);        
	        desc.setManifestVersion("1.0");
	        desc.setUsesManifest(true);
	        desc.setExportOutputFolders(true);
	        desc.setExportClassFiles(true);
	        desc.setExportJavaFiles(true);
	        desc.setElements(classes);
	        desc.setComment("GWT Project Tools Plug-i n");
	        
	        // Create JAR builder
	        IJarBuilder writer = desc.createFatJarBuilder();
	        
	        // Start building
	        writer.open(desc, null, null);
	        
	        // Add classes to JAR
	        IWorkspaceRoot root = project.getWorkspace().getRoot();
	        int output = root.getFolder(referenced.getOutputLocation()).getProjectRelativePath().segmentCount();
	        for(IFile file : classes) {
	        	writer.writeFile(file, file.getProjectRelativePath().removeFirstSegments(output));
	        }
	        
	        // Close writer
	        writer.close();
	        
	        // Set property which tell the builder to do nothing on resulting delta
	        project.getFile(dst).setPersistentProperty(DERIVED, TRUE);
	        
	    }	
	}

}